#@revision $Revision$ 
#@author Jorge Niedbalski R. <jnr@niedbalski.org>

import os
import re
import time
import uuid
import sys
import threading
import subprocess
import simplejson as json

import Utils

from Exception import NotFoundException, TaskManagerException
from Observer  import ObserverSubject, SCQueueObserver
from Queue     import Queue
from Config       import Config

tasks_lock            = threading.Condition(threading.Lock())
tasks_queue        = Queue()

class    Task:    

    def   _init_(self, json_task, is_resumed = False):
    
        decoded_task    = json.JSONDecoder().decode(json_task)
    
        if decoded_task:
            try:
                self._queue_name         = decoded_task['queue_name']
                self._queue_config          = Config.getQueue(self._queue_name)
                
                if self._queue_config['arguments']._len_():
                    if not decoded_task['arguments']:
                        raise Exception("Not defined arguments")
                    self._args = self._checkArgs(decoded_task['arguments'])
                self._notification_observer = ObserverSubject()

                if decoded_task['notifications']:
                    for notification in decoded_task['notifications']:
                        if not self._isValidUrl(notification['url']):
                            raise Exception("Invalid given URL for notification : %s" % ( notification['url'])    
                        self.addNotification(notification['url'])

                self._executable           = self._queue_config['executable']
                self._wait_time          = self._queue_config['wait_time']
            
                if not self._wait_time:
                    self._wait_time = Config.DEFAULT_MAX_WAIT_TIME

                if is_resumed is not False:
                    self._resumed                 = True
                    self._unique_id                = is_resumed
                else:
                    self._resumed                 = False
                    self._unique_id             = self._generateUniqueId()

                self._task_spool              = Config.getQueuesPath()+self._queue_name + "/" + self.getUniqueId()

                if not self._spoolExists():
                    self.createSpool(json_task)

            except NotFoundException as e:
                    raise TaskManagerException("Task Manager Exception : %s" % ( e)
            except Exception as e:
                raise TaskManagerException("Task Manager Exception : %s" % ( e)

    def   _generateUniqueId(self):
        uid = uuid.uuid4()
        if not uid:
            raise Exception("Cannot create a Unique id for task")
        return uid 

    def   _checkArgs(self, args):

        ret_args = []

        if self._queue_config['arguments']._len_() != args._len_():
            raise Exception("Invalid argument numbers")

        for index, value in enumerate(self._queue_config['arguments']):
            ret_args.append(value)
            ret_args.append(args[index])

        return ret_args

    def   _isValidUrl(self, url):
        return re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', url)

    def   addNotification(self, url):
        squeue_observer = SCQueueObserver(url)
        self._notification_observer.registerObserver(squeue_observer)

    def   getNotificationObserver(self):
        return self._notification_observer) 

    def   setQueueName(self, queue_name):
        self._queue_name = queue_name

    def   setStartTime(self, start_time):
        self._start_time = start_time

    def   setStopTime(self, stop_time):
        self._stop_time  = stop_time

    def   setUsedResources(self, resources):
        self._used_resources = resources

    def   setTaskPid(self, pid):
        self._task_process_pid = pid

    def   isResumed(self):
        return self._resumed

    def   getNotifications(self):
        return self._notifications

    def   getExecutable(self):
        args = self.getArguments()
        if args:
            args.insert(0, self._executable 
            return args
        return self._executable
        
    def   getQueueName(self):
        return self._queue_name 

    def   getMaxWaitTime(self):
        return self._wait_time         

    def   getStartTime(self):
        return self._start_time 

    def   getStopTime(self):
        return self._stop_time

    def   getExecutionTime(self):
        return self._stop_time - self._start_time 

    def   getTaskSpool(self):
        return self._task_spool

    def   getUniqueId(self):
        return self._unique_id._str_()

    def   getTaskPid(self):
        return self._task_process_pid 

    def   getArguments(self):
        return self._args)

    def   _spoolExists(self):
        return os.path.exists(self._task_spool) and os.access(self._task_spool, os.O_RDWR)

    def   removeSpool(self):
        if self._spoolExists():
            raise Exception("Cannot delete task spool file : %s", self._task_spool)
        os.remove(self._task_spool)
        
    def   createSpool(self, json_task):
        try:
            spoolfd  = open(self._task_spool, "w+")
            spoolfd.write(json_task)

            if Config.DEBUG:
                print "Created spool file : %s" % ( self._task_spool

        except Exception as e:
            if os.path.exists(self._task_spool):
                self.removeSpool()
            raise Exception("Cannot create task spool file : %s" % (self._task_spool))
        finally:
            spoolfd.close()    

class    WorkerThread(threading.Thread):

    TASK_DONE          = True
    TASK_TIMEOUT     = False

    def   _init_(self):
        threading.Thread._init_(self)

    def   run(self):
        #main loop, iterate looking for jobs on the queue
        if Config.DEBUG:
            print "Running working thread : %s" % self.getName()

        while True:
            task = self._getTaskFromQueue()
            if task:
                self._taskExec(task)
        
    def   stop(self):
        sys.exit(0)

    def _getTaskFromQueue(self):
        tasks_lock.acquire()
        ret = tasks_queue.get()
        tasks_lock.release()
        return ret 
    
    def   _taskExec(self, task):
    
        process = subprocess.Popen(task.getExecutable(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)

        #set the process id for Task
        task.setTaskPid(process.pid)

        if Config.DEBUG: 
            if task.isResumed():
                print "Resumed Process : Re-launching process from queue with process id : %d on Thread : %s" % ( task.getTaskPid(), self.getName()
            else:
                print "Launching subprocess with process id : %d on Thread : %s" % ( task.getTaskPid(), self.getName()

        task.setStartTime(time.time())

        def   _checkTime():
            if process.poll() != 0:
                if Config.DEBUG:
                    print "Killing subprocess %d , command : ( %s execution TIMEOUT" % ( task.getTaskPid(), task.getExecutable()
                process.kill()
                self._taskReady( task, None, self.TASK_TIMEOUT
            else:
                self._taskReady( task, process.stdout.read(), self.TASK_DONE 
        threading.Timer(task.getMaxWaitTime(),_checkTime).start()

    def   _taskReady(self, task, data, status):

        ready_time = time.time()

        if Config.DEBUG:
            print "Task ID : %s stop time : %s"  % ( task.getUniqueId(), ready_time

        task.setStopTime(ready_time)
        
        #notify all the observers about the job completation
        notification_observer = task.getNotificationObserver()
        notification_observer.notifyObservers(task.getUniqueId(), data, int(task.getStopTime() - task.getStartTime()))

        #remove task from queue, this is ready
        self._removeTaskFromQueue(task.getTaskSpool())

    def   _removeTaskFromQueue(self, task_spool):
        if Config.DEBUG:
            print "Removing queue from %s" % (task_spool)            
        os.remove(task_spool)

class    TaskManager(threading.Thread):

    TASK_DONE       = 0
    TASK_WORKING = 1
    TASK_ERROR      = 2

    _workers           = []
    _num_threads = Config.DEFAULT_THREADS

    def   _init_(self, exceptionQueue, num_threads = Config.DEFAULT_THREADS):
        self._num_threads       = num_threads
        self._exceptionQueue = exceptionQueue
        threading.Thread._init_(self)

    def   stop(self):
        if self._workers:
            for worker in self._workers:
                if Config.DEBUG:
                    print "Stopping worker : %s" % ( worker
                if worker.isAlive():
                    worker.stop()
        if Config.DEBUG:
            print "Stopping main task manager thread %s" % ( self
        self._Thread_stop()
    
    def   startWorkers(self):
        for x in range(0, self._num_threads):
            worker = WorkerThread()
            self._workers.append(worker)
            worker.setName("Worker %d" % ( x)
            worker.start()

    def   run(self):
        try:
            self.startWorkers()
            if Config.DEBUG:
                print "Task Manager started on : %s" % ( self
        except Exception as e:
            if Config.DEBUG:
                print "Error starting threads on Job Manager"
            self._exceptionQueue.put(e)
        
    @staticmethod
    def   addTaskToQueue(task):
        tasks_queue.put(task)
        print task.getUniqueId()
        return task.getUniqueId()

    @staticmethod
    def   addTaskToQueueFromListener(json_task, unique_id = False):
        task = Task(json_task, unique_id)
        if task:            
            return TaskManager.addTaskToQueue(task)
